/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2003-2008, University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import org.dom4j.DocumentException;

import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import edu.umd.cs.findbugs.config.UserPreferences;
import edu.umd.cs.findbugs.filter.FilterException;
import edu.umd.cs.findbugs.util.Util;

/**
 * Helper class to parse the command line and configure
 * the IFindBugsEngine object.
 * As a side-effect it also configures a DetectorFactoryCollection
 * (to enable and disable detectors as requested).
 */
public class TextUICommandLine extends FindBugsCommandLine {
	/**
	 * Handling callback for choose() method,
	 * used to implement the -chooseVisitors and -choosePlugins options.
	 */
	private interface Chooser {
		/**
		 * Choose a detector, plugin, etc.
		 *
		 * @param enable whether or not the item should be enabled
		 * @param what   the item
		 */
		public void choose(boolean enable, String what);
	}

	private static final int PRINTING_REPORTER = 0;
	private static final int SORTING_REPORTER = 1;
	private static final int XML_REPORTER = 2;
	private static final int EMACS_REPORTER = 3;
	private static final int HTML_REPORTER = 4;
	private static final int XDOCS_REPORTER = 5;

	private int bugReporterType = PRINTING_REPORTER;
	private boolean relaxedReportingMode = false;
	private boolean useLongBugCodes = false;
	private boolean showProgress = false;
	private boolean xmlMinimal = false;
	
	private boolean xmlWithMessages = false;
	private boolean xmlWithAbridgedMessages = false;
	private String stylesheet = null;
	private boolean quiet = false;
	private ClassScreener classScreener = new ClassScreener();
	private List<String> includeFilterFile = new LinkedList<String>();
	private List<String> excludeFilterFile = new LinkedList<String>();
	private List<String> excludeBugFile = new LinkedList<String>();
	private boolean setExitCode = false;
	private boolean noClassOk = false;
	private int priorityThreshold = Detector.NORMAL_PRIORITY;
	private int rankThreshold = 20;
	private PrintStream outputStream = null;
	private Set<String> bugCategorySet = null;
	private UserPreferences userPreferences;
	private String trainingOutputDir;
	private String trainingInputDir;
	private String releaseName = "";
	private String projectName="";
	private String sourceInfoFile = null;
	private boolean xargs = false;
	private boolean scanNestedArchives = true;
	private boolean applySuppression;

	/**
	 * Constructor.
	 */
	public TextUICommandLine() {

		addSwitch("-showPlugins", "show list of available detector plugins");

		startOptionGroup("Output options:");
		addSwitch("-timestampNow", "set timestamp of results to be current time");
		addSwitch("-quiet", "suppress error messages");
		addSwitch("-longBugCodes", "report long bug codes");
		addSwitch("-progress", "display progress in terminal window");
		addOption("-release", "release name", "set the release name of the analyzed application");
		addSwitch("-experimental", "report all warnings including experimental bug patterns");
		addSwitch("-low", "report all warnings");
		addSwitch("-medium", "report only medium and high priority warnings [default]");
		addSwitch("-high", "report only high priority warnings");
		addOption("-maxRank", "rank", "only report issues with a bug rank at least as scary as that provided");
		addSwitch("-sortByClass", "sort warnings by class");
		addSwitchWithOptionalExtraPart("-xml", "withMessages",
				"XML output (optionally with messages)");
		addSwitch("-xdocs", "xdoc XML output to use with Apache Maven");
		addSwitchWithOptionalExtraPart("-html", "stylesheet",
				"Generate HTML output (default stylesheet is default.xsl)");
		addSwitch("-emacs", "Use emacs reporting format");
		addSwitch("-relaxed", "Relaxed reporting mode (more false positives!)");
		addSwitchWithOptionalExtraPart("-train", "outputDir",
				"Save training data (experimental); output dir defaults to '.'");
		addSwitchWithOptionalExtraPart("-useTraining", "inputDir",
				"Use training data (experimental); input dir defaults to '.'");
		addOption("-sourceInfo", "filename",
				"Specify source info file (line numbers for fields/classes)");
		addOption("-projectName", "project name", "Descriptive name of project");

		addOption("-outputFile", "filename", "Save output in named file");
		addOption("-output", "filename", "Save output in named file");
		makeOptionUnlisted("-outputFile");
		addSwitchWithOptionalExtraPart("-nested", "true|false",
		"analyze nested jar/zip archives (default=true)");

		startOptionGroup("Output filtering options:");
		addOption("-bugCategories", "cat1[,cat2...]", "only report bugs in given categories");
		addOption("-onlyAnalyze", "classes/packages", "only analyze given classes and packages; end with .* to indicate classes in a package, .- to indicate a package prefix");
		addOption("-excludeBugs", "baseline bugs", "exclude bugs that are also reported in the baseline xml output");
		addOption("-exclude", "filter file", "exclude bugs matching given filter");
		addOption("-include", "filter file", "include only bugs matching given filter");
		addSwitch("-applySuppression", "Exclude any bugs that match suppression filter loaded from fbp file");
		
		startOptionGroup("Detector (visitor) configuration options:");
		addOption("-visitors", "v1[,v2...]", "run only named visitors");
		addOption("-omitVisitors", "v1[,v2...]", "omit named visitors");
		addOption("-chooseVisitors", "+v1,-v2,...", "selectively enable/disable detectors");
		addOption("-choosePlugins", "+p1,-p2,...", "selectively enable/disable plugins");
		addOption("-adjustPriority", "v1=(raise|lower)[,...]",
				"raise/lower priority of warnings for given visitor(s)");

		startOptionGroup("Project configuration options:");
		addOption("-auxclasspath", "classpath", "set aux classpath for analysis");
		addSwitch("-auxclasspathFromInput", "read aux classpath from standard input");
		addOption("-sourcepath", "source path", "set source path for analyzed classes");
		addSwitch("-exitcode", "set exit code of process");
		addSwitch("-noClassOk", "output empty warning file if no classes are specified");
		addSwitch("-xargs", "get list of classfiles/jarfiles from standard input rather than command line");
		
		}

	@Override
	public Project getProject() {
		return project;
	}
	public boolean getXargs() {
		return xargs;
	}
	public boolean setExitCode() {
		return setExitCode;
	}
	public boolean noClassOk() {
		return noClassOk;
	}

	public boolean quiet() {
		return quiet;
	}

	public boolean applySuppression() {
		return applySuppression;
	}
	
	@SuppressWarnings("DM_EXIT")
	@Override
	protected void handleOption(String option, String optionExtraPart) {
		if (option.equals("-showPlugins")) {
			System.out.println("Available plugins:");
			int count = 0;
			for (Iterator<Plugin> i = DetectorFactoryCollection.instance().pluginIterator(); i.hasNext(); ) {
				Plugin plugin = i.next();
				System.out.println("  " + plugin.getPluginId() + " (default: " +
						(plugin.isEnabled() ? "enabled" : "disabled") + ")");
				if (plugin.getShortDescription() != null)
					System.out.println("    Description: " + plugin.getShortDescription());
				if (plugin.getProvider() != null)
					System.out.println("    Provider: " + plugin.getProvider());
				if (plugin.getWebsite() != null)
					System.out.println("    Website: " + plugin.getWebsite());
				++count;
			}
			if (count == 0) {
				System.out.println("  No plugins are available (FindBugs installed incorrectly?)");
			}
			System.exit(0);
		} else if (option.equals("-experimental"))
			priorityThreshold = Detector.EXP_PRIORITY;
		else if (option.equals("-longBugCodes"))
			useLongBugCodes = true;
		else if (option.equals("-progress")) {
			showProgress = true;
		}
		else if (option.equals("-timestampNow"))
			project.setTimestamp(System.currentTimeMillis());
		else if (option.equals("-low"))
			priorityThreshold = Detector.LOW_PRIORITY;
		else if (option.equals("-medium"))
			priorityThreshold = Detector.NORMAL_PRIORITY;
		else if (option.equals("-high"))
			priorityThreshold = Detector.HIGH_PRIORITY;
		else if (option.equals("-sortByClass"))
			bugReporterType = SORTING_REPORTER;
		else if (option.equals("-xml")) {
			bugReporterType = XML_REPORTER;
			if (!optionExtraPart.equals("")) {
				if (optionExtraPart.equals("withMessages"))
					xmlWithMessages = true;
				else if (optionExtraPart.equals("withAbridgedMessages")) {
					xmlWithMessages = true;
					xmlWithAbridgedMessages = true;
				} else if (optionExtraPart.equals("minimal")) {
					xmlWithMessages = false;
					xmlMinimal = true;
				} else
					throw new IllegalArgumentException("Unknown option: -xml:" + optionExtraPart);
			}
		} else if (option.equals("-emacs")) {
			bugReporterType = EMACS_REPORTER;
		} else if (option.equals("-relaxed")) {
			relaxedReportingMode = true;
		} else if (option.equals("-train")) {
			trainingOutputDir = !optionExtraPart.equals("") ? optionExtraPart : ".";
		} else if (option.equals("-useTraining")) {
			trainingInputDir = !optionExtraPart.equals("") ? optionExtraPart : ".";
		} else if (option.equals("-html")) {
			bugReporterType = HTML_REPORTER;
			if (!optionExtraPart.equals("")) {
				stylesheet = optionExtraPart;
			} else {
				stylesheet = "default.xsl";
			}
		} else if (option.equals("-xdocs")) {
			bugReporterType = XDOCS_REPORTER;
		} else if (option.equals("-applySuppression")) {
			applySuppression = true;
		} else if (option.equals("-quiet")) {
			quiet = true;
		} else if (option.equals("-nested")) {
			scanNestedArchives =
				optionExtraPart.equals("") || Boolean.valueOf(optionExtraPart).booleanValue();
		} else if (option.equals("-exitcode")) {
			setExitCode = true;
		} else if (option.equals("-auxclasspathFromInput")) {
			try {
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			while (true) {
				String s = in.readLine();
				if (s == null) break;
				addAuxClassPathEntries(s);
			}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
			
		} else if (option.equals("-noClassOk")) {
			noClassOk = true;
		} else if (option.equals("-xargs")) {
			xargs = true;
		} else {
			super.handleOption(option, optionExtraPart);
		}
	}

	@SuppressWarnings("DM_EXIT")
	@Override
	protected void handleOptionWithArgument(String option, String argument) throws IOException {
		if (option.equals("-outputFile") || option.equals("-output")) {
			File outputFile = new File(argument);
			String extension = Util.getFileExtension(outputFile);
			if (bugReporterType == PRINTING_REPORTER && (extension.equals("xml") || extension.equals("fba")))
					bugReporterType = XML_REPORTER;

			try {
				outputStream = new PrintStream(new BufferedOutputStream(new FileOutputStream(outputFile)));
			} catch (IOException e) {
				System.err.println("Couldn't open " + outputFile + " for output: " + e.toString());
				System.exit(1);
			}
		} else if (option.equals("-maxRank")) {
			this.rankThreshold = Integer.parseInt(argument);
		} else if (option.equals("-projectName")) {
			this.projectName = argument;
		} else if (option.equals("-release")) {
			this.releaseName = argument;
		} else if (option.equals("-sourceInfo")) {
			sourceInfoFile = argument;
		} else if (option.equals("-visitors") || option.equals("-omitVisitors")) {
			boolean omit = option.equals("-omitVisitors");

			if (!omit) {
				// Selecting detectors explicitly, so start out by
				// disabling all of them.  The selected ones will
				// be re-enabled.
				getUserPreferences().enableAllDetectors(false);
			}

			// Explicitly enable or disable the selected detectors.
			StringTokenizer tok = new StringTokenizer(argument, ",");
			while (tok.hasMoreTokens()) {
				String visitorName = tok.nextToken().trim();
				DetectorFactory factory = DetectorFactoryCollection.instance().getFactory(visitorName);
				if (factory == null)
					throw new IllegalArgumentException("Unknown detector: " + visitorName);
				getUserPreferences().enableDetector(factory, !omit);
			}
		} else if (option.equals("-chooseVisitors")) {
			// This is like -visitors and -omitVisitors, but
			// you can selectively enable and disable detectors,
			// starting from the default set (or whatever set
			// happens to be in effect).
			choose(argument, "Detector choices", new Chooser() {
				public void choose(boolean enabled, String what) {
					DetectorFactory factory = DetectorFactoryCollection.instance().getFactory(what);
					if (factory == null)
						throw new IllegalArgumentException("Unknown detector: " + what);
					if (FindBugs.DEBUG) {
						System.err.println("Detector " + factory.getShortName() + " " +
								(enabled ? "enabled" : "disabled") +
								", userPreferences="+System.identityHashCode(getUserPreferences()));
					}
					getUserPreferences().enableDetector(factory, enabled);
				}
			});
		} else if (option.equals("-choosePlugins")) {
			// Selectively enable/disable plugins
			choose(argument, "Plugin choices", new Chooser() {
				public void choose(boolean enabled, String what) {
					Plugin plugin = DetectorFactoryCollection.instance().getPluginById(what);
					if (plugin == null)
						throw new IllegalArgumentException("Unknown plugin: " + what);
					plugin.setEnabled(enabled);
				}
			});
		} else if (option.equals("-adjustPriority")) {
			// Selectively raise or lower the priority of warnings
			// produced by specified detectors.

			StringTokenizer tok = new StringTokenizer(argument, ",");
			while (tok.hasMoreTokens()) {
				String token = tok.nextToken();
				int eq = token.indexOf('=');
				if (eq < 0)
					throw new IllegalArgumentException("Illegal priority adjustment: " + token);

				String adjustmentTarget = token.substring(0, eq);
				String adjustment = token.substring(eq + 1);
				
				int adjustmentAmount;
				if (adjustment.equals("raise"))
					adjustmentAmount = -1;
				else if (adjustment.equals("lower"))
					adjustmentAmount = +1;
				else if (adjustment.equals("suppress"))
					adjustmentAmount = +100;
				else 
					throw new IllegalArgumentException("Illegal priority adjustment value: " +
							adjustment);

				
				DetectorFactory factory = DetectorFactoryCollection.instance().getFactory(adjustmentTarget);
				if (factory != null) 
					factory.setPriorityAdjustment(adjustmentAmount);
				else {
					// 
					I18N i18n = I18N.instance();
					BugPattern pattern = i18n.lookupBugPattern(adjustmentTarget);
					if (pattern == null)
						throw new IllegalArgumentException("Unknown detector: " + adjustmentTarget);
					pattern.adjustPriority(adjustmentAmount);
				}
			
		 
			}
		} else if (option.equals("-bugCategories")) {
			this.bugCategorySet = FindBugs.handleBugCategories(getUserPreferences(), argument);
		} else if (option.equals("-onlyAnalyze")) {
			// The argument is a comma-separated list of classes and packages
			// to select to analyze.  (If a list item ends with ".*",
			// it specifies a package, otherwise it's a class.)
			StringTokenizer tok = new StringTokenizer(argument, ",");
			while (tok.hasMoreTokens()) {
				String item = tok.nextToken();
				if (item.endsWith(".-"))
					classScreener.addAllowedPrefix(item.substring(0, item.length() - 1));
				else if (item.endsWith(".*"))
					classScreener.addAllowedPackage(item.substring(0, item.length() - 1));
				else
					classScreener.addAllowedClass(item);
			}
		} else if (option.equals("-exclude")) {
			excludeFilterFile.add(argument);
		} else if (option.equals("-excludeBugs")) {
			excludeBugFile.add(argument);
		} else if (option.equals("-include")) {
			includeFilterFile.add(argument);
		} else if (option.equals("-auxclasspath")) {
			addAuxClassPathEntries(argument);
		} else if (option.equals("-sourcepath")) {
			StringTokenizer tok = new StringTokenizer(argument, File.pathSeparator);
			while (tok.hasMoreTokens())
				project.addSourceDir(new File(tok.nextToken()).getAbsolutePath());
		} else {
			super.handleOptionWithArgument(option, argument);
		}
	}

	/**
	 * Parse the argument as auxclasspath entries and add them
     * @param argument
     */
    private void addAuxClassPathEntries(String argument) {
	    StringTokenizer tok = new StringTokenizer(argument, File.pathSeparator);
	    while (tok.hasMoreTokens())
	    	project.addAuxClasspathEntry(tok.nextToken());
    }

	/**
	 * Common handling code for -chooseVisitors and -choosePlugins options.
	 *
	 * @param argument the list of visitors or plugins to be chosen
	 * @param desc     String describing what is being chosen
	 * @param chooser  callback object to selectively choose list members
	 */
	private void choose(String argument, String desc, Chooser chooser) {
		StringTokenizer tok = new StringTokenizer(argument, ",");
		while (tok.hasMoreTokens()) {
			String what = tok.nextToken().trim();
			if (!what.startsWith("+") && !what.startsWith("-"))
				throw new IllegalArgumentException(desc + " must start with " +
						"\"+\" or \"-\" (saw " + what + ")");
			boolean enabled = what.startsWith("+");
			chooser.choose(enabled, what.substring(1));
		}
	}

	public void configureEngine(IFindBugsEngine findBugs) throws IOException, FilterException {
		// Load plugins
		DetectorFactoryCollection.instance().ensureLoaded();

		// Set the DetectorFactoryCollection (that has been configured
		// by command line parsing)
		findBugs.setDetectorFactoryCollection(DetectorFactoryCollection.instance());

		TextUIBugReporter textuiBugReporter;
		switch (bugReporterType) {
		case PRINTING_REPORTER:
			textuiBugReporter = new PrintingBugReporter();
			break;
		case SORTING_REPORTER:
			textuiBugReporter = new SortingBugReporter();
			break;
		case XML_REPORTER:
			{
				XMLBugReporter xmlBugReporter = new XMLBugReporter(project);
				xmlBugReporter.setAddMessages(xmlWithMessages);
				xmlBugReporter.setMinimalXML(xmlMinimal);
				
				textuiBugReporter = xmlBugReporter;
			}
			break;
		case EMACS_REPORTER:
			textuiBugReporter = new EmacsBugReporter();
			break;
		case HTML_REPORTER:
			textuiBugReporter = new HTMLBugReporter(project, stylesheet);
			break;
		case XDOCS_REPORTER:
			textuiBugReporter = new XDocsBugReporter(project);
			break;
		default:
			throw new IllegalStateException();
		}

		if (quiet)
			textuiBugReporter.setErrorVerbosity(BugReporter.SILENT);

		textuiBugReporter.setPriorityThreshold(priorityThreshold);
		textuiBugReporter.setRankThreshold(rankThreshold);
		textuiBugReporter.setUseLongBugCodes(useLongBugCodes);

		if (findBugs instanceof IFindBugsEngine2) 
			((IFindBugsEngine2)findBugs).setRankThreshold(rankThreshold);
		if (outputStream != null)
			textuiBugReporter.setOutputStream(outputStream);

		BugReporter bugReporter = textuiBugReporter;

		if (bugCategorySet != null) {
			bugReporter = new CategoryFilteringBugReporter(bugReporter, bugCategorySet);
		}

		findBugs.setBugReporter(bugReporter);
		findBugs.setProject(project);
		
		if (showProgress) {
			findBugs.setProgressCallback(new TextUIProgressCallback(System.out));
		}

		findBugs.setUserPreferences(getUserPreferences());
		for(String s : excludeBugFile) 
	        try {
	            findBugs.excludeBaselineBugs(s);
            } catch (DocumentException e) {
	           throw new IOException("Unable to read " + excludeBugFile + ":" + e.getMessage());
            }
        for(String s : includeFilterFile) 
			findBugs.addFilter(s, true);
        for(String s : excludeFilterFile) 
			findBugs.addFilter(s, false);

		
		
		findBugs.setClassScreener(classScreener);

		findBugs.setRelaxedReportingMode(relaxedReportingMode);
		findBugs.setAbridgedMessages(xmlWithAbridgedMessages);

		if (trainingOutputDir != null) {
			findBugs.enableTrainingOutput(trainingOutputDir);
		}
		if (trainingInputDir != null) {
			findBugs.enableTrainingInput(trainingInputDir);
		}

		if (sourceInfoFile != null) {
			findBugs.setSourceInfoFile(sourceInfoFile);
		}

		findBugs.setAnalysisFeatureSettings(settingList);

		findBugs.setReleaseName(releaseName);
		findBugs.setProjectName(projectName);

		findBugs.setScanNestedArchives(scanNestedArchives);
		findBugs.setNoClassOk(noClassOk);

		if (applySuppression)
			findBugs.setApplySuppression(true);
		
		findBugs.finishSettings();
	}

	/**
	 * Handle -xargs command line option by reading
	 * jar file names from standard input and adding them
	 * to the project.
	 * 
	 * @throws IOException
	 */
	public void handleXArgs() throws IOException {
		if (getXargs()) {
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			while (true) {
				String s = in.readLine();
				if (s == null) break;
				project.addFile(s);
			}
		}
	}

	/**
	 * @return Returns the userPreferences.
	 */
	private UserPreferences getUserPreferences() {
		if (userPreferences == null) 
			userPreferences = UserPreferences.createDefaultUserPreferences();
		return userPreferences;
	}
}
